16.Gün - SwiftUI Temelleri Proje-1 Bölüm-1
Table of Contents
Proje-1 boyunca, SwiftUI’nin temellerine bir giriş yapacağız. Proje boyunca WeSplit isimli uygulamayı oluşturacağız. Bu uygulama hesabı ortak olarak ödemek maksadıyla hesabı kişi sayısına bölmekte ve bahşişi hesaplamaktadır. Bugünkü yazımızda Form
, NavigationStack
ve @State
kavramlarını inceleyeceğiz.
Bu proje aynı zamanda GitHub’da da bulunmaktadır.
GitHub - GorkemGuray/WeSplit: 100 Days of SwiftUI - Project-1
Xcode Yeni SwiftUI Projesi Nasıl Oluşturulur #
Xcode’u başlatın ve ardından “Create New Project” seçeneğini seçin.
Burada karşımıza bazı seçenekler listesi çıkacak buradan önce iOS’u ardından App kısmını seçerek Next butonuna basıyoruz.
Karşımıza aşağıdaki gibi bir ekran çıkacak;
Bu ekranda yapmamız gerekenler;
- Product Name için “WeSplit” yazın.
- Organization Identifier için istediğinizi girebilirsiniz, ancak bir web siteniz varsa bileşenleri ters çevrilmiş olarak girmelisiniz “gorkem.co” , “co.gorkem” olacaktır. Eğer bir alan adınız yoksa, bu kısma “me.soyad.isim” şeklinde de giriş yapabilirsiniz.
- Interface için SwiftUI’ı seçin.
- Language için Swift’i seçin.
- Storage için None seçeneğini seçin.
- Alttaki tüm onay kutularının işaretli olmadığından emin olun.
Bir SwiftUI Uygulamasının Temel Yapısı #
Xcode’da soldaki bölüm project navigator olarak adlandırılmaktadır. Burada göreceğimiz dosyalar;
- WeSplitApp.swift : Uygulamayı başlatacak (launch) kodu içerir. Eğer uygulama başlatıldığında bir şey oluşturup ve uygulama çalıştığı süre boyunca bunu canlı tutmak istersek buraya koymamız gerekir.
- ContentView.swift : Uygulamamız için ilk kullanıcı arayüzünü (User Interface-UI) içerir ve bu projede tüm işi yapacağımız yerdir.
- Asset.xcassets : asset kataloğudur. Asset, uygulamada kullanmak istediğimiz resimlerden oluşan bir koleksiyondur. Buraya renkleri, uygulama simgelerini, iMessage sticker gibi şeyleri ekleyebiliriz.
- Preview Content : Bu bir gruptur. İçinde Preview Assets.xcassets bulunur. Bu da başka bir asset kataloğudur. Fakat bu sefer kullanıcı arayüzlerini tasarlarken kullanmak istediğimiz örnek resimler içindir, uygulama çalışırken nasıl görünebilecekleri hakkında bir fikir verir.
Not : Project navigator ‘de bulunan dosyalarınızın uzantılarını görmüyorsanız, Xcode > Settings > General sekmesinde bulunan File Extension seçeneğini kontrol edin.
Bu proje için tüm çalışmalarımız Xcode’un bizim için oluşturduğu ContentView.swift dosyasında gerçekleşecek. Xcode’un bu dosyada oluşturduğu kod aşağıdadır;
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}
Kendi kodumuzu yazmaya başlamadan önce tüm bunların ne işe yaradığını gözden geçirelim.
-
import SwiftUI
: Swift’e SwiftUI Framework tarafından bize verilen tüm işlevleri kullanmak istediğimizi söyler. Apple bize birçok framework sağlamaktadır; machine learning, ses oynatma, görüntü işleme ve daha bir sürü şey. Bu sebeple programımızın her şeyi kullanmak istediğini varsaymak yerine, hangi parçaları kullanmak istediğimizi söyleriz, böylece sadece o kısımlar yüklenebilir. -
struct ContentView: View
ContentView
adında yeni bir struct oluşturur ve bununView
protocol’e uygun olduğunu söyler.View
, yukarıda import ettiğimiz SwfitUI’dan gelmektedir ve ekranda çizmek istediğimiz her şey tarafından benimsenmesi gereken temel protocol’dür. -
var body: some View
body
adında yeni bir computed property(hesaplanmış değişken) tanımlar ve bu property ilginç bir türe sahiptir :some View
. Bu bizim layout’umuzunView
protocol’e uygun bir şey döndüreceği anlamına gelir. Arka planda layout’umuzdaki herşeye bağlı olan karmaşık bir veri türünün döndürülmesiyle sonuçlanacaktır ancaksome View
bu konuda bütün işi devralarak gerisini halleder. Bakınız: Opaque Return TypeView
protocol’ün tek bir gereksinimi vardır o dasome View
döndürenbody
adında bir computed variable’a sahip olmamızdır. view struct’larımıza daha fazla property ve method ekleyebiliriz tabiki, ancakbody
protocol tarafından zorunlu tutulan tek şeydir. -
VStack
ve içindeki kod altında “Hello, world!” metni bulunan bir küre görüntüsü gösterir. Bu küre görüntüsü Apple’ın SF Symbols icon setinden gelmektedir. Text view’lar ekrana çizilen basit statik metin parçalarıdır ve gerektiğinde otomatik olarak birden fazla satır olabilirler. -
imageScale()
,foregroundStyle()
vepadding()
görüntü veVStack
üzerinde çağrılan methodlardır. SwiftUI’ın modifier olarak adlandırdığı bu methodların küçük bir farkı bulunmaktadır: her zaman hem orijinal verilerimizi hem de istediğimiz ekstra değişikliği içeren yeni bir view döndürürler.
ContentView
struct’ın altında, içinde ContentView()
olan #Preview
’ı göreceksiniz. Bu aslında App Store’a giden uygulamamızın bir parçası olmayacak, bunun yerine özellikle Xcode’un oluşturduğumuz kullanıcı arayüzünün önizlemesini (preview) gösterebilmesi için özel bir kod parçasıdır.
Bu önizlemeler, genellikle kodumuzun sağında görünen canvas adı verilen Xcode özelliğini kullanır.İstersek önizleme kodunu özelleştirebiliriz, bu yalnızca canvas’ı etkiler çalıştırılan gerçek uygulamayı değiştirmez.
Önemli : Eğer Canvas’ı göremiyorsanız, Xcode penceresindeki Editor menüsünden Canvas’ı seçebilirsiniz.
Çoğu zaman kodumuzdaki bir hatanın Xcode canvas güncellemesini durduğunu göreceğiz. Bunu düzeltmek için refresh butonunu ya da Option+Cmd+P klavye kısayolunu kullanabiliriz.
SwiftUI Form Oluşturma #
Pek çok uygulama, kullanıcılarının bir tür girdi (input) girmesini gerektirir. SwiftUI bize bunun için Form
adında özel bir view sunar. Formlar, metin ve resimler gibi statik kontrollerin scroll listeleridir. Ancak text field, toggle switch, button vb. gibi kullanıcı etkileşimli kontrolleri de içerebilir.
Bir text view’ı aşağıdaki gibi Form
’un içine koyarak temel bir Form oluşturabiliriz.
var body: some View {
Form {
Text("Hello, world!")
}
}
Xcode canvasına baktığımızda oldukça değiştiğini görebiliriz;
Burada, tıpkı Ayarlar uygulamasında göreceğimiz gibi bir liste başlangıcı görmekteyiz. Daha da fazla satır ekleyebiliriz;
Form {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
Hatta, bir formun içine istediğimiz kadar çok şey bulundurabiliriz, Örneğin bu kod on satırlık bir liste gösterecektir.
Form {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
Eğer Formumuzu tıpkı Ayarlar uygulamasında olduğu gibi görsel parçalara bölmek istiyorsak, Section
’ı kullanabiliriz.
Form {
Section {
Text("Hello, world!")
}
Section {
Text("Hello, world!")
Text("Hello, world!")
}
}
Bir formu ne zaman bölümlere ayırmamız gerektiği ile ilgili kesin ve katı bir kural yoktur. Sadece ilgili öğeleri görsel olarak gruplandırmak için vardır.
SwiftUI Navigation Bar Ekleme #
iOS üstteki sistem saatinin ve alttaki ana ekran göstergesinin altı da dahil olmak üzere ekranın herhangi bir yerine içerik yeleştirmemize izin verir. Fakat bu alanların hepsini kullanırsak hoş olmayan görünümler ortaya çıkabilir. Bu sebeple SwiftUI varsayılan olarak bileşenlerin sistem kullanıcı arayüzü veya cihazın yuvarlatılmış köşeleri tarafından kapatılmayacakları bir alana yerleştirilmesini sağlıyor. Bu alan safe area olarak isimlendirilmektedir.
iPhone 15’te safe area dinamik adanın hemen altından ana ekran göstergesinin hemen üstüne kadar olan alanı kapsıyor. Bunun gibi bir kod ile bunu rahatlıkla görebiliriz.
Bu kodu iOS simülatörde çalıştırılalım. Simülatörü çalıştırmak için Xcode penceresinin sol üstünde bulunan play tuşuna basabilir veya Cmd+R kısayolunu kullanabiliriz.
Formun dinamik adanın altında başladığını göreceksiniz, bu nedenle varsayılan olarak formumuzdaki satır tamamen görünür durumdadır. Formlar kaydırılabilir (scroll), simülatörde formu yukarı doğru kaydırırsak, satırı saatin altına girecek şekilde yukarı taşıyabiliriz, ki bu da istemediğimiz bir durumdur. Çünkü her ikisinin de okunmasını zorlaştıracaktır.
Bunu düzeltmenin yaygın yolu, ekranın üst kısmına bir navigation bar eklemekten geçer. Navigation bar başlıklara ve düğmelere sahip olabilir. Ayrıca SwiftUI’de kullanıcı bir eylem gerçekleştirdiğinde bize yeni görünümler gösterme yeteneği de verir.
Navigation bar’ı şu şekilde ekleyebiliriz.
var body: some View {
NavigationStack {
Form {
Section {
Text("Hello, world!")
}
}
}
}
Yukarıdakş kodu yazdığımızda bir önceki ile birebir aynı gözükecektir. Fakat genellikle navigation bar’larda başlık’da kullanırız. Bu başlığı (title) modifier yardımıyla ekleyebiliriz.
NavigationStack {
Form {
Section {
Text("Hello, world!")
}
}
.navigationTitle("SwiftUI")
}
.navigationTitle()
modifier’ını form’a eklediğimizde, Swift aslında bir navigation bar ve sağladığımız tüm mevcut içeriklere sahip yeni bir form oluşturur.
Navigation bar’a bir title eklediğimizde, bu title için büyük bir yazı tipi kullanıldığını fark edeceğiz. Başka bir modifier ekleyerek küçük bir yazı tipi elde edebiliriz.
.navigationBarTitleDisplayMode(.inline)
SwiftUI State nedir? Program State’in Değiştirilmesi #
View’lar state’lerinin bir fonksiyonudur. (Views are a function of their state)
SwiftUI’nin view’larının state’lerinin bir fonksiyonu olduğunu söylediğimizde, kullanıcı arayüzünün nasıl göründüğünün programımızın state’i tarafından belirlendiğini kastediyoruz. Örneğin, kullanıcılar bir text field’a kendi isimlerini girene kadar Devam butonuna dokunamazlar.
Bunu, ismi ve dokunulduğunda çalıştırılacak bir action closure’u olan buton ile gösterebiliriz.
struct ContentView: View {
var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
tapCount += 1
}
}
}
Bu kod oldukça makul görünüyor: “Tap Count” yazan bir buton oluşturun ve düğmeye kaç kez dokunulduğunu belirtin, ardından butona her dokunulduğunda tapCount
değişkenine 1 ekleyin.
Ancak bu kod Xcode tarafından derlenmeyecektir. Gördüğünüz gibi ContentView
sabit olarak oluşturulan bir struct’tır. Eğer struct’lar hakkında öğrendiklerimizi hatırlarsak, bu onun değişmez (immutable) olduğu anlamına gelir (değelerini serbestçe değiştiremeyiz).
Property’lerini değiştirmek isteyen struct methodları oluştururken, mutating
keyword’ünü eklememiz gerekir: mutating func doSomeWork()
gibi. Ancak Swift mutated computed property yapmamıza izin vermez, bu da mutating var body: some View
yazamayacağımız anlamına gelir.
Bu, bir çıkmaza girmişiz gibi görünebilir: Programımız çalışırken değerleri değiştirebilmek istiyoruz, ancak view’lar struct olduğu için Swift bize izin vermiyor.
Neyse ki Swift bize property wrapper adı verilen özel bir çözüm sunuyor: property’lerimizin önüne yerleştirebileceğimiz ve onlara süper güçler kazandıran özel bir nitelik. Bir butona kaç kez dokunulduğu gibi basit program state’lerini depolamak için SwiftUI’den @State
adlı bir property wrapper ‘ı aşağıdaki gibi kullanabiliriz.
struct ContentView: View {
@State var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
self.tapCount += 1
}
}
}
Bu küçük değişiklik programımızın çalışması için yeterlidir. Şimdi onu oluşturabilir ve derleyebiliriz.
@State
struct’ların sınırlamalarını aşmamızı sağlar. Struct’lar sabit (constant) olduğu için property’lerini değiştiremeyeceğimizi biliyoruz, ancak @State
bu değerin SwiftUI tarafından değiştirilebilecek bir yerde ayrı olarak saklanmasına izin verir.
İpucu : SwiftUI’da program state’ini saklamanın çeşitli yolları vardır. @State
özellikle tek bir view’da depolanan basit property’ler için tasarlanmıştır. Sonuç olarak Apple, bu property’lere aşağıdaki gibi private
access control eklememizi önerir.
@State private var tapCount = 0
State’i Kullanıcı Arayüzü Kontrollerine Bağlama (Binding State to User Interface Controls) #
SwiftUI’nin @State
property wrapper’ı, view struct’ları özgürce değiştirmemize olanak tanır. Bu da programımız değiştikçe view property’lerini de buna uygun olarak güncelleyebileceğimiz anlamına gelir.
Ancak, kullanıcı arayüzü kontrollerinde işler biraz daha karmaşıktır. Örneğin, kullanıcıların içine yazabileceği düzenlenebilir bir metin kutusu oluşturmak istiyorsak, aşağıdaki gibi bir SwiftUI view’ı oluşturabiliriz.
struct ContentView: View {
var body: some View {
Form {
TextField("Enter your name")
Text("Hello, world!")
}
}
}
Yukarıdaki kod bir text field ve text view oluşturmaya çalışır. Ancak, SwiftUI text field alanındaki metnin nerede saklanacağını bilmek istediği için kod derlenemez.
Unutmayın view’lar state’lerinin bir fonksiyonudur. Bu text field yalnızca programımızda depolanan bir değeri yansıtıyorsa bir şey gösterebilir. SwiftUI’nin istediği şey, text field içinde gösterilebilecek ve kullanıcının text field’a yazdığı her şeyi saklayacak olan struct’daki bir string property’dir.
Şu şekilde bir değişiklik yapabiliriz;
struct ContentView: View {
var name = ""
var body: some View {
Form {
TextField("Enter your name", text: name)
Text("Hello, world!")
}
}
}
Bu, name
property ekler ve ardından bunu text field’ı oluşturmak için kullanır. Ancak kod yine de çalışmayacaktır çünkü Swift’in name
property’sini kullanıcının text field’a yazdığı her şeyle eşleşecek şekilde güncelleyebilmesi gerekir. Bu sebeple @State
’i şu şekilde kullanabiliriz.
@State private var name = ""
Ancak bu hala yeterli değil ve kodumuz hala derlenmiyor.
Sorun, Swift’in “bu property’nin değerini burada göster” ile “bu property’nin değerini burada göster, ancak tüm değişiklikleri property’ye geri yaz” arasında ayrım yapmasıdır.
Text Field söz konusu olduğunda, Swift’in metinde bulunan her şeyin name
property’de de bulunduğundan emin olması gerekir, böylece view’larımızın state’lerinin bir fonksiyonu olduğuna dair sözünü gerçekleştirebilir.
Buna two-way binding denir. text field’ı property’mizin değerini gösterecek şekilde bağlarız (bind), ancak aynı zamanda text field’da yapılan herhangi bir değişikliğin property’yi güncelleyeceği şekilde de bağlarız.
Swift’te bu two-way binding’leri daha görünür olmaları için özel bir sembolle işaretleriz: $
Bu Swift’e property’nin değerini okuması gerektiğini ama aynı zamanda herhangi bir değişiklik olduğunda bunu geri yazması gerektiğini söyler.
Yani struct’ın doğru versiyonu şu şekildedir.
struct ContentView: View {
@State private var name = ""
var body: some View {
Form {
TextField("Enter your name", text: $name)
Text("Hello, world!")
}
}
}
Devam etmeden önce, text view’ı kullanıcının adını doğrudan text field’ın altında gösterecek şekilde değiştirelim;
Text("Your name is \(name)")
Bunun $name
yerine name
şeklinde kullanıldığına dikkat ettiniz mi? Bunun nedeni burada two-way binding istememizdir. Değeri okumak istiyoruz evet, ancak bir şekilde geri yazmak istemiyoruz, çünkü bu text view değişmeyecektir.
Bu sebeple, bir property adından önce $
gördüğümüzde, bunun two-way binding olduğunu unutmamalıyız: property’nin değeri okunur ve aynı zamanda yazılır.
Döngü (Loop) içinde View Oluşturma #
Bir döngü içinde birkaç SwiftUI view’ı oluşturmayı istemek yaygındır. Örneğin, bir isim Array’i üzerinde döngü yapmak ve her birinin bir metin görünümü olmasını isteyebiliriz.
SwiftUI bize bu amaç için ForEach
adı verilen özel bir view türü sunar Bu Array ve range üzerinde döngü yaparak gerektiği kadar görünüm oluşturabilir.
ForEach
üzerinde döndüğü her öğe için bir kez closure çalıştırır ve geçerli döngü öğesini geçirir. Örneğin, 0’dan 100’e kadar döngü yaparsak, önce 0 sonra 1, sonra 2 ve bu şekilde devam eder.
Örneğin bu 100 satırlı bir form oluşturur;
Form {
ForEach(0..<100) { number in
Text("Row \(number)")
}
}
ForEach
closure içine parametre geçtiğinden, parametre adı için aşağıdaki gibi kısa sözdizimi kullanabiliriz.
Form {
ForEach(0 ..< 100) {
Text("Row \($0)")
}
}
ForEach
özellikle SwiftUI’nin Picker
view’ı ile çalışırken kullanışlıdır. Picker
kullanıcıların aralarından seçim yapabileceği çeşitli seçenekler göstermemizi sağlar.
Bunu göstermek için bir view tanımlayacağız;
- Olası öğrenci adlarından oluşan bir Array’e sahiptir.
- O an seçili olan öğrenciyi saklayan
@State
property’si vardır. - Kullanıcılardan favorilerini seçmelerini isteyen bir
Picker
view oluşturur ve@State
property’ye two-way binding kullanır. - Tüm olası öğrenci adları üzerinde döngü yapmak için
ForEach
’i kullanır ve bunları bir text view’e dönüştürür.
İşte kod;
struct ContentView: View {
let students = ["Harry", "Hermione", "Ron"]
@State private var selectedStudent = "Harry"
var body: some View {
NavigationStack {
Form {
Picker("Select your student", selection: $selectedStudent) {
ForEach(students, id: \.self) {
Text($0)
}
}
}
}
}
}
Burada çok fazla kod yok fakat bazı şeyleri açıklığa kavuşturmak gerekir;
students
Array’inin@State
ile işaretlenmesine gerek yoktur çünkü bu bir sabittir, değişmeyecektir.selectedStudent
property “Harry” değeri ile başlar ancak değişebilir, bu sebeple@State
ile işaretlenmiştir.Picker
kullanıcılara ne yaptığını söyleyen ve aynı zamanda ekran okuyucuları için açıklayıcı bir metin olan “Select your student” etiketine sahiptir.Picker
’ınselectedStudent
’e iki yönlü bağı vardır (two-way binding), yani “Harry” seçimini göstermeye başlayacak ancak kullanıcı başka bir şey seçtiğinde property’yi güncelleyecektir.ForEach
içinde tüm students Array üzerinde döngü yaparız.- Her student için o student’in adını gösteren bir text view oluşturuyoruz.
Buradaki tek kafa karıştırıcı kısım şudur : ForEach(students, id: \.self)
Bu, students Array’i üzerinde döngü yapar, böylece her bir için bir text view oluşturabiliriz, ancak id: \.self
kısmı önemlidir. Çünkü SwiftUI’nin ekrandaki her görünümü benzersiz bir şekilde tanımlayabilmesi gerekir, böylece işler değiştiğinde bunu algılayabilir.
Örneğin, array’i önece Ron gelecek şekilde düzenlersek, SwiftUI text view’ı aynı anda hareket ettirecektir. Bu nedenle, SwiftUI’ye string array’deki her bir öğeyi benzersiz bir şekilde nasıl tanımlayabileceğini söylememiz gerekir. Her bir stringi benzersiz kılan nedir?
Sadece basit stringlerden oluşan bir array’imiz var ve string ile ilgili tek benzersiz şey string’in kendisidir. Array’deki her bir string farklıdır, bu nedenle string’ler doğal olarak benzersizdir.
Dolayısıyla, birçok view oluşturmak için ForEach
kullandığımızda ve SwiftUI bize string array’deki her bir öğeyi benzersiz yapan tanımlayıcının ne olduğunu sorduğunda, cevabımız \.self
, yani “stringlerin kendileri benzersizdir” olacaktır. Elbette, bu students array’e yinelenen string’ler eklersek sorun yaşayabileceğimiz anlamına gelir, ancak bu örnekte sorun yok.
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.